# -*- coding: utf-8 -*-
# # # # # # # # # # # # # # # # # # # # #
#                                       #
#    Trabajo Práctico 3 - Conexiones    #
#                                       #
#     Teoría de las Comunicaciones      #
#      Departamento de Computación      #
#              FCEN - UBA               #
#           octubre de 2013             #
#                                       #
# # # # # # # # # # # # # # # # # # # # #


from collections import defaultdict
import threading
import random
import time
#import tablib

from common import PacketBuilder, ProtocolControlBlock
from soquete import Soquete
from packet import ACKFlag, FINFlag, SYNFlag
from worker import ClientProtocolWorker
from buffers import DataBuffer, RetransmissionQueue, NotEnoughDataException
from constants import MIN_PACKET_SIZE, MAX_PACKET_SIZE, CLOSED,\
        ESTABLISHED, FIN_SENT, SYN_SENT, MAX_SEQ,\
        MAX_RETRANSMISSION_ATTEMPTS


class ClientControlBlock(ProtocolControlBlock):
    def __init__(self, address, port, size):
        ProtocolControlBlock.__init__(self, address, port)
        # Próximo SEQ a enviar
        self.send_seq = random.randint(1, MAX_SEQ)
        # Tamaño de la ventana de emisión
        self.send_window = size
        # Límite inferior de la ventana (i.e., unacknowledged)
        self.window_lo = self.send_seq
        # Límite superior de la ventana
        self.window_hi = self.modular_sum(self.window_lo, self.send_window)

    def increment_send_seq(self):
        self.send_seq = self.modular_increment(self.send_seq)

    def slide_window(self, packet):
        seqno = packet.get_ack_number()
        # Chequear validez
        if (self.window_lo < self.window_hi and
            (seqno < self.window_lo or seqno > self.window_hi)):
            raise Exception("Suspicious packet number")
        if (self.window_hi < self.window_lo and
            (seqno > self.window_hi and seqno < self.window_lo)):
            raise Exception("Suspicious packet number")
        # Asumo que el paquete viene en buena fe

        self.window_lo = self.modular_increment(seqno)
        self.window_hi = self.modular_sum(self.window_lo, self.send_window)

    def get_send_seq(self):
        return self.send_seq

    def get_send_window(self):
        return self.send_window

    # Responde True sii la ventana de emisión no está saturada.
    def send_allowed(self):
        return self.send_seq != self.window_hi


class PTCClientProtocol(object):
    def __init__(self, address, port, size):
        self.retransmission_queue = RetransmissionQueue(self)
        self.retransmission_attempts = defaultdict(int)
        self.retrans_count = 0
        self.outgoing_buffer = DataBuffer()
        self.state = CLOSED
        self.control_block = ClientControlBlock(address, port, size)
        self.socket = Soquete(address, port)
        self.packet_builder = PacketBuilder(self)

#        self.tab_data = tablib.Dataset(headers=['Time', 'Packet #', 'Size', 'Event', 'RTT', 'Extra'])

        # self.error como se pide en la seccion de timeout
        self.error = []

    def is_connected(self):
        return self.state == ESTABLISHED

    def build_packet(self, payload=None, flags=None):
        seq = self.control_block.get_send_seq()
        self.control_block.increment_send_seq()
        packet = self.packet_builder.build(payload=payload, flags=flags, seq=seq)
        return packet

    def send_packet(self, packet):
        self.socket.send(packet)

    def send_and_queue_packet(self, packet):
        if FINFlag in packet.get_flags():
            flags = 'FIN'
        elif SYNFlag in packet.get_flags():
            flags = 'SYN'
        else:
            flags = 'SEND'
        print '[Client-Log] Queing packet %s' % packet.get_seq_number()
#        self.tab_data.append((time.time(), packet.get_seq_number(), len(packet.get_payload()), flags, '', ''))
        self.send_packet(packet)
        self.retransmission_queue.put(packet)

    def send(self, data):
        if not self.is_connected():
            raise Exception('cannot send data: connection not established')
#        self.tab_data.append((time.time(), '', len(data), 'USER_SEND', '', ''))
        self.worker.send(data)

    def connect_to(self, address, port):
        self.worker = ClientProtocolWorker.spawn_for(self)
        self.worker.start()
        self.connected_event = threading.Event()
        self.control_block.set_destination_address(address)
        self.control_block.set_destination_port(port)

        syn_packet = self.build_packet(flags=[SYNFlag])
        self.state = SYN_SENT
        self.send_and_queue_packet(syn_packet)

        self.connected_event.wait()

    def handle_timeout(self):
        ###################
        ##   Completar!  ##
        ###################

        # (1) Obtener los paquetes en self.retransmission_queue
        # (2) Volver a enviarlos
        # (3) Reencolarlos para otra eventual retransmisión
        # ...y verificar que no se exceda la cantidad máxima de reenvíos!
        # (hacer self.shutdown() si esto ocurre y dejar un mensaje en self.error)

        print "[Client-Log] Timeout received"
        temporary_queue = []
        for packet in self.retransmission_queue:
            if (self.retransmission_attempts.get(packet.get_seq_number(), 0) >=
                    MAX_RETRANSMISSION_ATTEMPTS):
                self.error.append("Packet exceeded retransmission attempts")
                print "[Client-Log] Packet %s exceeded retransmission attempts" % packet.get_seq_number()
#                self.tab_data.append(
#                    (time.time(), packet.get_seq_number(), len(packet.get_payload()),
#                     'TIMEOUT_SHUTDOWN', '', ''))
                self.shutdown()
            else:
                self.retrans_count += 1
                temporary_queue.append(packet)

        self.retransmission_queue.clear()

        for packet in temporary_queue:
                print "[Client-Log] Resending packet: %s" % packet.get_seq_number()
#                self.tab_data.append(
#                    (time.time(), packet.get_seq_number(), len(packet.get_payload()),
#                     'TIMEOUT_RETRANSMIT', '', 'Attempts: %s' % self.retransmission_attempts[
#                     packet.get_seq_number()]))
                self.send_and_queue_packet(packet)
                self.retransmission_attempts[packet.get_seq_number()] += 1

    def handle_pending_data(self):
        more_data_pending = False

        if self.control_block.send_allowed():
            try:
                data = self.outgoing_buffer.get(MIN_PACKET_SIZE, MAX_PACKET_SIZE)
            except NotEnoughDataException:
                return

            packet = self.build_packet(payload=data)
            self.send_and_queue_packet(packet)

            if not self.outgoing_buffer.empty():
                more_data_pending = True

        if more_data_pending:
            self.worker.signal_pending_data()

    def handle_incoming(self, packet):
        ###################
        ##   Completar!  ##
        ###################

        # Tener en cuenta que se debe:
        # * Corroborar que el flag de ACK esté seteado
        # Buscar paquete en retransmission queue a ver si está.
        # Si está, pasar a estado ESTABLISHED
        # * No olvidar de hacer self.connected_event.set() al confirmar el ACK y establecer la conexión!!!


        flags = packet.get_flags()
        if not ACKFlag in flags:
            raise Exception("No ACK flag provided")

        print "[Client-Log] Received packet with ack: %s" % packet.get_ack_number()
        if self.state == SYN_SENT:
            # Checkear no. de ACK no de SEQ
            if self.retransmission_queue.packet_list[0][0].get_seq_number() == packet.get_ack_number():
                self.state = ESTABLISHED

                self.retransmission_queue.acknowledge(packet)
                self.control_block.slide_window(packet)
                self.connected_event.set()
            else:
                pass # This is not the packet you are looking for
        elif self.state == ESTABLISHED:
            # Checkear RTT
            #ret_packet = [p for p in self.retransmission_queue.packet_list if
            #                   p[0].get_seq_number() == packet.get_ack_number()][0]
            #packet_send_time = ret_packet[1]
            #rtt = time.time() - packet_send_time
            #self.tab_data.append((time.time(), packet.get_ack_number(), len(ret_packet[0].get_payload()), 'RCVD', rtt, ''))

            self.retransmission_queue.acknowledge(packet)
            self.control_block.slide_window(packet)

            if self.control_block.send_allowed() and not self.outgoing_buffer.empty():
                self.worker.signal_pending_data()

        elif self.state == FIN_SENT:
            # Solo buscar no. de secuencia del ultimo enviado
            fin_packetno =  [p.get_seq_number() for p in self.retransmission_queue if FINFlag in p]
            if len(fin_packetno) != 0:
                self.shutdown()
                # self.state = CLOSED
                # self.retranmission_queue.clear()
            else:
                print "Paquete no reconocido para FIN"
            # Si recibo el ACK del paquete, pasar a CLOSED
        # * Distinguir el caso donde el estado es SYN_SENT

        # * DONE Analizar si #ACK es aceptado (hablar con el bloque de control para hacer este checkeo)
        # * DONE Sacar de la cola de retransmisión los paquetes reconocidos por #ACK
        # * DONE Ajustar la ventana deslizante con #ACK
        # * Tener en cuenta también el caso donde el estado es FIN_SENT

    def handle_close_connection(self):
        if not self.outgoing_buffer.empty():
            self.worker.signal_pending_data()
            self.worker.signal_close_connection()
        elif not self.retransmission_queue.empty():
            self.worker.signal_close_connection()
        else:
            print '[Client-Log] Queing FIN'
            fin_packet = self.build_packet(flags=[FINFlag])
            self.send_and_queue_packet(fin_packet)
            self.state = FIN_SENT

#    def write_tab_data(self):
#        with open('data/data_%s.csv' % time.time(), 'w') as f:
#            f.write(self.tab_data.csv)

    def has_pending_data(self):
        return (not self.outgoing_buffer.empty() or
                not self.retransmission_queue.empty())
    
    def get_retrans_count(self):
        return self.retrans_count

    def close(self):
        if self.is_connected():
            self.worker.signal_close_connection()

    def wait_for_close(self):
        print '[Client-Log] Waiting for pending data'
        while self.has_pending_data():
            time.sleep(0.001)

        print '[Client-Log] Stopping workers'
        self.worker.stop()
        print '[Client-Log] Joining workers'
        self.worker.join()
        print '[Client-Log] Done joining'

    def shutdown(self):
        self.outgoing_buffer.clear()
        self.retransmission_queue.clear()
        self.retransmission_attempts.clear()
        self.worker.stop()
        # Esto es por si falló el establecimiento de conexión (para destrabar al thread principal)
        self.connected_event.set()
        self.state = CLOSED
